﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Castle.DynamicProxy;
using System.Reflection;
using System.Runtime.Serialization;
using Castle.Core.Interceptor;
using Sio.Mdm.Types;

namespace Sio.Mdm.Entities
{
    interface IEntityInterceptor : IInterceptor
    {
        Type EntityType { get; }
        IEntity ProxyInstance { get; set; }
    }

    [Serializable]
    class EntityInterceptor : IEntityInterceptor, IEntity
    {
        #region Fields
        [NonSerialized]
        private static Dictionary<string, string> BaseProperties = new Dictionary<string, string>();
        [NonSerialized]
        private Dictionary<string, object> OldValues = new Dictionary<string, object>();
        [NonSerialized]
        private Dictionary<string, MulticastDelegate> EntityChanges = new Dictionary<string, MulticastDelegate>();
        [NonSerialized]
        private Dictionary<string, bool> EventChanges = new Dictionary<string, bool>();        
        #endregion

        #region Construction
        public EntityInterceptor()
            : this(typeof(IEntity))
        {
        }
        public EntityInterceptor(Type type)            
        {
            _entityType = type;
        }
        static EntityInterceptor()
        {
            PropertyInfo[] properties = typeof(IEntity).GetProperties();

            foreach (PropertyInfo prop in properties)
            {
                BaseProperties.Add(prop.Name, prop.Name);
            }
        }
        #endregion

        #region Properties

        #region EntityType
        private Type _entityType;
        public Type EntityType
        {
            get
            {
                return _entityType;
            }
        }        
        #endregion

        #region ProxyInstance
        [NonSerialized]
        private IEntity _proxyInstance;
        public IEntity ProxyInstance
        {
            get
            {
                return _proxyInstance;
            }
            set
            {
                _proxyInstance = value;
            }
        } 
        #endregion

        #region Info
        [NonSerialized]
        private EntityFactory.EntityInfo _info;
        internal EntityFactory.EntityInfo Info
        {
            get
            {
                if (_info == null)
                {
                    _info = EntityFactory.EntitiesInfo[EntityType];
                }

                return _info;
            }
        } 
        #endregion

        #region EntityState
        private EntityState _state = EntityState.Unknown;
        public EntityState EntityState
        {
            get
            {
                return Error == null? _state: EntityState.Error;
            }
            internal set
            {
                _state = value;
            }
        } 
        #endregion

        #region Error
        private Sio.Mdm.Types.Result _error;
        public Sio.Mdm.Types.Result Error
        {
            get
            {
                return _error;
            }
        }
        #endregion

        #region Values
        private Dictionary<string, object> _values;
        public Dictionary<string, object> Values
        {
            get
            {
                if (null == _values)
                {
                    _values = new Dictionary<string, object>();
                }

                return _values;
            }
        }
        #endregion

        #region IsLoaded
        private bool IsLoaded
        {
            get
            {
                return EntityState == EntityState.Unchanged ||
                    EntityState == EntityState.Created ||
                    EntityState == EntityState.Changed;
            }
        }
        #endregion

        #region FieldsName
        private string[] _fieldsName = null;
        /// <summary>
        /// Get defined fields in entity
        /// </summary>
        public string[] FieldsName
        {
            get
            {
                if (null == _fieldsName)                
                {
                    int count = 0;
                    _fieldsName = new string[Info.Fields.Count];
                    
                    foreach (KeyValuePair<string, FieldAttribute> pair in Info.Fields)
                    {
                        _fieldsName[count++] = pair.Value.Name;
                    }
                }

                return _fieldsName;
            }
        }
        #endregion
       
        #endregion

        #region Methods
        
        #region Public
        
        #region IInterceptor Members
        object Intercept(IInvocation invocation, params object[] args)
        {
            if (invocation.Method.DeclaringType == typeof(IEntity))
            {
                try
                {
                    return SelfInvoke(invocation);
                }
                catch (Exception ex)
                {
                    ex.ToString();
                }
            }

            // TODO: Filter property methods with attributes
            #region Field manipulation
            if (invocation.Method.Name.StartsWith("get_") ||
                    invocation.Method.Name.StartsWith("set_"))
            {
                string keyName = invocation.Method.Name.Remove(0, 4).ToLower();
                bool isGetter = invocation.Method.Name.StartsWith("get_");

                if (isGetter)
                {
                    if (BaseProperties.ContainsKey(keyName))
                    {
                        return GetPropertyValue(keyName);
                    }

                    // One of property value
                    if (!Values.ContainsKey(keyName))
                    {
                        Type retType = invocation.Method.ReturnType;
                        FieldAttribute attrib = Info.Fields[keyName];

                        // create either default value type (by default engine will do it for us)
                        // or simply return <null>
                        if (retType.IsValueType)
                        {
                            if (attrib.DefaultValue == null)
                            {
                                Values[keyName] = Activator.CreateInstance(retType);
                            }
                            else
                            {
                                Values[keyName] = attrib.DefaultValue;
                            }
                        }
                        else
                        {
                            if (typeof(IEntity).IsAssignableFrom(retType))
                            {
                                IEntity entity = EntityFactory.CreateInstance(retType);

                                // create entity instance
                                Values[keyName] = entity;

                                // TODO: Rethink strategy here. This could be called
                                // 1000 or more times                                
                                if (attrib.LoadOnDemand && IsLoaded)
                                {
                                    LoadField(entity, retType, attrib);
                                    //entity.Load(0);
                                }
                            }
                            else
                            {
                                // Set property to default since it's reference type
                                // Probably and likley null
                                Values[keyName] = attrib.DefaultValue;
                            }
                        }
                    }

                    //by matejg
                    object defValue = null;
                    //set some custom "default" values for nullable types
                    if (invocation.Method.ReturnType == typeof(int))
                        defValue = Info.Fields[keyName].DefaultValue != null ? Info.Fields[keyName].DefaultValue : -1;
                    else if (invocation.Method.ReturnType == typeof(Int64))
                        defValue = Info.Fields[keyName].DefaultValue != null ? Info.Fields[keyName].DefaultValue : Convert.ToInt64(-1);
                    else if (invocation.Method.ReturnType == typeof(DateTime))
                        defValue = Info.Fields[keyName].DefaultValue != null ? Info.Fields[keyName].DefaultValue : DateTime.MinValue;
                    else if (invocation.Method.ReturnType == typeof(bool))
                        defValue = Info.Fields[keyName].DefaultValue != null ? Info.Fields[keyName].DefaultValue : false;
                    if (Info.IsSettingsEntity)
                    {
                        return Info.InvokeSettingGet(keyName);
                    }

                    return Values[keyName] is System.DBNull ? defValue : Values[keyName];
                    //end by matejg
                }
                else
                {
                    // One of predefined property in IEntity interface
                    if (BaseProperties.ContainsKey(keyName))
                    {
                        return SetProperty(keyName, args[0]);
                    }

                    _error = Info.Validate(ProxyInstance, keyName, args[0]);

                    // We got validation error
                    if (Error != null)
                    {
                        _error = new EntityValidationError();
                        return args[0];
                    }

                    // Change state
                    if (EntityState == EntityState.Unchanged)
                    {
                        EntityState = EntityState.Changed;
                    }

                    // Promote to created from unknown
                    if (EntityState == EntityState.Unknown)
                    {
                        EntityState = EntityState.Created;
                    }

                
                    if (EntityState == EntityState.Changed)
                    {
                        object oldVal = null;
                        if (Values.ContainsKey(keyName))
                            oldVal = Values[keyName];
                        TrackChanges(keyName.ToLower(), oldVal, args[0]);
                    }
                

                    object result = Values[keyName] = args[0];

                    if (Info.IsSettingsEntity)
                    {
                        Info.InvokeSettingSet(keyName, result);
                    }
                    
                    // This will not reflect over remote instances.
                    // And it's better not. Performance degradation could 
                    // be to high. It's also the reason why we avoided
                    // MarshalByRef here.
                    InvokeChangeEvent(keyName);

                    return result;
                }
            }

            #endregion

            #region Method handling
            if (Info.Methods.ContainsKey(invocation.Method.Name))
            {
                MethodAttribute attribute = Info.Methods[invocation.Method.Name];
                return attribute.InvokeMethod(ProxyInstance, invocation.Arguments);
            }
            #endregion

            #region Events handling
            // Assign proper delegate to instance in case notifications are used
            if (invocation.Method.Name.StartsWith("add_") ||
                invocation.Method.Name.StartsWith("remove_"))
            {
                bool isRemove = invocation.Method.Name.StartsWith("remove_");
                string keyName = invocation.Method.Name.Replace("add_", "").Replace("remove_", "");

                if (!EventChanges.ContainsKey(keyName))
                {
                    if (isRemove)
                    {
                        // TODO: remove field notification
                        EntityChanges.Remove(keyName);
                        EventChanges.Remove(keyName);

                        return args[0];
                    }
                    else
                    {
                        EventInfo ev = EntityType.GetEvent(keyName);

                        if (ev != null)
                        {
                            NotifyChangeAttribute attr =
                                EntityFactory.EntityInfo.GetAttribute<NotifyChangeAttribute>(
                                    ev.GetCustomAttributes(true)
                                );

                            // Seems we found notification event. Store and return delegate.
                            // Doing nothing will throw excpetion since compiler did 
                            // not implement adding and removin functions for us 
                            // (this is in reality just interface)
                            if (attr != null)
                            {
                                MulticastDelegate handler = args[0] as MulticastDelegate;

                                if (String.IsNullOrEmpty(attr.FieldName))
                                {
                                    EntityChanges.Add(keyName, handler);
                                }

                                EventChanges.Add(keyName, true);
                                return handler;
                            }
                        }
                    }
                }
            } 
            #endregion

            return null;
        }
        void IInterceptor.Intercept(IInvocation invocation)
        {
            invocation.ReturnValue = Intercept(invocation, invocation.Arguments);
        }

        private void TrackChanges(string fieldName, object oldValue, object newValue)
        {
            if ((oldValue == null) || (newValue != null))
                    OldValues.Add(fieldName, oldValue);
            else if (!oldValue.Equals(newValue))
            {
                if (OldValues.ContainsKey(fieldName))
                {
                    if (OldValues[fieldName].Equals(newValue))
                        OldValues.Remove(fieldName);
                }
                else
                    OldValues.Add(fieldName, oldValue);
            }
        }

        #endregion

        #region IEntity Members
        bool IEntity.Create()
        {
            EntityState = EntityState.Created;
            return true;
        }
        bool IEntity.Delete()
        {
            EntityState = EntityState.Deleted;
            return (this as IEntity).Update();
        }
        bool IEntity.Load<T>(ISioFilter<T> criteria)
        {   
            _error = Info.Select(this, criteria);

            if (_error == null)
            {
                EntityState = EntityState.Unchanged;
            }

            if (_error is EntityNotFoundResult)
            {
                EntityState = EntityState.Unknown;
            }

            return _error == null;
        }
        bool IEntity.Update()
        {
            if (EntityState == EntityState.Unknown ||
                EntityState == EntityState.Error)
            {
                return false;
            }

            if (EntityState == EntityState.Unchanged)
            {
                return true;
            }

            _error = Info.Update(ProxyInstance);

            if (_error == null)
            {
                EntityState = EntityState.Unchanged;
            }

            return _error == null;
        }
        void IEntity.SetField(string name, object value)
        {
            name = name.ToLower();
            
            if (!Info.Fields.ContainsKey(name))
            {
                return;
            }

            if (Info.Fields[name].FieldType == typeof(bool) && !(value is bool))
            {
                //tole v primeru bool vrednosti odleti z exceptionom (value je short), zato "po kmečko" spremenil matej...
                /*
                if(value is IComparable)
                {
                    value = (value as IComparable).CompareTo(0) != 0;
                }
                else
                {
                    value = value != null;
                }
                 */
                //by matejg
                value = value.ToString().Equals("1") ? true : false;
                //end by matejg
            }

            Values[name.ToLower()] = value;

            if (Info.IsSettingsEntity)
            {
                Info.InvokeSettingSet(name, value);
            }
        }
        object IEntity.GetField(string name)
        {
            if (Info.IsSettingsEntity)
            {
                name = name.ToLower();

                if (!Values.ContainsKey(name.ToLower()))
                {
                    Values[name] = Info.InvokeSettingGet(name);
                }
                
                return Values[name];
            }
            else
            {
                name = name.ToLower();
                
                if (!Values.ContainsKey(name))
                {
                    return null;
                }

                return Values[name];
            }
        }
        T IEntity.GetField<T>(string name)
        {
            if (Info.IsSettingsEntity)
            {
                return (T)Values[name.ToLower()];
            }
            else
            {
                name = name.ToLower();
                
                if (!Values.ContainsKey(name))
                {
                    return default(T);
                }

                return (T)Values[name];
            }
        }
        void IEntity.SetState(EntityState state)
        {
            _state = state;
            switch (state)
            {
                case EntityState.Unchanged:
                case EntityState.Created:
                    OldValues.Clear();
                    break;
            }
        }
        bool IEntity.Validate()
        {
            foreach(KeyValuePair<string, FieldAttribute> field in Info.Fields)
            {
                IList<ValidationAttribute> validators = Info.GetValidators(field.Key);

                foreach (ValidationAttribute attr in validators)
                {
                    if ((EntityState & attr.ValidationState) > 0)
                    {
                        if (!attr.Validate(this, field.Key, Values[field.Key]))
                        {
                            return false;
                        }
                    }
                }
            }

            return true;
        }
        Dictionary<string, object> IEntity.GetOldValues()
        {
            return OldValues;
        }
        #endregion
        
        #endregion

        #region Private

        #region SetPropertyValue, GetPropertyValue
        private object GetPropertyValue(string name)
        {
            return GetType().InvokeMember(
                name,
                BindingFlags.GetProperty,
                null,
                this,
                null
            );
        }

        private object SetProperty(string name, object value)
        {
            return GetType().InvokeMember(
                name,
                BindingFlags.SetProperty,
                null,
                this,
                new object[] { value }
            );
        } 
        #endregion

        #region InvokeChangeEvent
        private void InvokeChangeEvent(string fieldName)
        {
            List<MulticastDelegate> handlers = new List<MulticastDelegate>();

            foreach (KeyValuePair<string, MulticastDelegate> pair in EntityChanges)
            {
                handlers.Add(pair.Value);
            }

            foreach (MulticastDelegate handler in handlers)
            {
                handler.DynamicInvoke(ProxyInstance, fieldName);
            }
        }
        #endregion

        #region SelfInvoke
        private object SelfInvoke(IInvocation invocation)
        {
            return invocation.Method.Invoke(this, invocation.Arguments);
        }
        #endregion

        #region LoadField
        private void LoadField(IEntity entity, Type entityType, FieldAttribute attribute)
        {
            if (entity is IEntityCollection)
            {
                LoadCollection(entity as IEntityCollection, attribute);
            }
            else
            {
                LoadEntity(entity, entityType, attribute);
            }
        }

        private void LoadEntity(IEntity entity, Type entityType, FieldAttribute attribute)
        {
            if (attribute.SelectMethod != Info.SelectMethod)
            {
            }
            else
            {
                EntityFactory.EntityInfo info = EntityFactory.GetEntityInfo(entityType);
                //JURE POGLEJ
                //Criteria criteria = new Criteria();
                
                //criteria.Query.SetExpression(new QueryParameter(attribute.Name, ProxyInstance.GetField(attribute.Name)));
                //_error = info.Select(entity, criteria);
                _error = info.Select(entity, null);
            }
        }

        private void LoadCollection(IEntityCollection entity, FieldAttribute attribute)
        {   
        }
        #endregion

        #endregion
                
        #endregion       
    }
}
